//
//  QRCode.ScanViewController.swift
//  SwiftCode
//
//  Created by yuhus on 2024/1/24.
//

import Foundation
import UIKit
import AVFoundation
import RxSwift

extension QRCode {
  private enum SessionSetupResult {
    case success
    case notAuthorized
    case configurationFailed
  }
  
  final class ScanViewController: BaseViewController {
    private let holder = ScanViewHolder()
    private let viewModel = ScanViewModel()
    
    private let session = AVCaptureSession()
    
    private var isSessionRunning = false
    
    // Communicate with the session and other session objects on this queue.
    private let sessionQueue = DispatchQueue(label: "QRCode.session.queue")
    
    private var setupResult: SessionSetupResult = .success
        
    private var previewView: PreviewView = PreviewView()
    
    private let metadataOutput = AVCaptureMetadataOutput()
    
    private let resultSubject = PublishSubject<String>()
    
    override var shouldAutorotate: Bool { false }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait }
    
    override func viewDidLoad() {
      super.viewDidLoad()
      
      setupView()
      bindData()
      
      authorizationStatus()
      /*
       设置捕获会话。
       一般来说，同时从多个线程改变 AVCaptureSession 或其任何输入、输出或连接是不安全的。
       不要在主队列上执行这些任务，因为 AVCaptureSession.startRunning() 是一个阻塞调用，这可能会需要很长时间。
       将会话设置分派到 sessionQueue，这样主队列就不会被阻塞，从而保持 UI 的响应能力。
       */
      sessionQueue.async {
        self.configureSession()
      }
    }
    
    override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
      sessionQueue.async {
        switch self.setupResult {
        case .success:
          self.session.startRunning()
          self.isSessionRunning = self.session.isRunning
          DispatchQueue.main.async {
            self.configurePreviewView()
          }
        case .notAuthorized:
          DispatchQueue.main.async {
            QRCode.presentNotAuthorized()
          }
        case .configurationFailed:
          DispatchQueue.main.async {
            QRCode.presentConfigurationFailed()
          }
        }
      }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
      sessionQueue.async {
        if self.setupResult == .success {
          self.session.stopRunning()
          self.isSessionRunning = self.session.isRunning
        }
      }
      super.viewWillDisappear(animated)
    }
    
    private func setupView() {
      view.backgroundColor = .black
      navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
      
      // Set up the video preview view.
      previewView.session = session
      previewView.videoPreviewLayer.videoGravity = .resizeAspectFill
      
      view.addSubview(previewView)
      previewView.snp.makeConstraints { $0.edges.equalToSuperview() }
      
      holder.inflate(view)
    }
    
    private func configurePreviewView() {
      let targetRect = holder.scanView.frame
      if targetRect.width > 0, targetRect.height > 0 {
        metadataOutput.rectOfInterest = previewView.videoPreviewLayer.metadataOutputRectConverted(fromLayerRect: targetRect)
      }
      holder.updateMaskLayer()
    }
    
    private func bindData() {
      let output = viewModel.adapt(input: .init(
        viewDidLoad: viewDidLoadSubject.asObservable(),
        backButtonTapped: backButton.rx.tap.asObservable()
      ))
    }
    
    private func authorizationStatus() {
      switch AVCaptureDevice.authorizationStatus(for: .video) {
      case .authorized: // 用户之前已授予对相机的访问权限。
        break
      case .notDetermined:
        /*
         The user has not yet been presented with the option to grant
         video access. Suspend the session queue to delay session
         setup until the access request has completed.
         
         Note that audio access will be implicitly requested when we
         create an AVCaptureDeviceInput for audio during session setup.
         */
        sessionQueue.suspend()
        AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
          if !granted {
            self.setupResult = .notAuthorized
          }
          self.sessionQueue.resume()
        })
        
      default:
        // The user has previously denied access.
        setupResult = .notAuthorized
      }
    }
    
    private func configureSession() {
      if setupResult != .success {
        return
      }
      
      session.beginConfiguration()
      
      // Add video input.
      do {
        let backVideoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(
          deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera],
          mediaType: .video, position: .back
        )
        let defaultVideoDevice = backVideoDeviceDiscoverySession.devices.first ?? AVCaptureDevice.default(for: .video)
        
        guard let videoDevice = defaultVideoDevice else {
          setupResult = .configurationFailed
          session.commitConfiguration()
          return print("Default video device is unavailable.")
        }
        
        let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
        
        if session.canAddInput(videoDeviceInput) {
          session.addInput(videoDeviceInput)
        } else {
          setupResult = .configurationFailed
          session.commitConfiguration()
          return print("Couldn't add video device input to the session.")
        }
      } catch {
        setupResult = .configurationFailed
        session.commitConfiguration()
        return print("Couldn't create video device input: \(error)")
      }
      
      // Add the metadata output.
      if session.canAddOutput(metadataOutput) {
        session.addOutput(metadataOutput)
        metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
        metadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]
      } else {
        setupResult = .configurationFailed
        session.commitConfiguration()
        return print("Could not add metadata output to the session")
      }
      
      session.commitConfiguration()
    }
    
    private lazy var backButton: UIButton = {
      let button = UIButton()
      button.setImage(R.image.qrcode_scan_back(), for: .normal)
      button.widthAnchor.constraint(equalToConstant: 34).isActive = true
      button.heightAnchor.constraint(equalToConstant: 34).isActive = true
      return button
    }()
  }
}

// MARK: - Camera Delegate.
extension QRCode.ScanViewController: AVCaptureMetadataOutputObjectsDelegate {
  func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
    guard let first = metadataObjects.first,
          let readableCode = first as? AVMetadataMachineReadableCodeObject,
          let result = readableCode.stringValue else {
      return
    }
    resultSubject.onNext(result)
    print(result)
  }
}
